package com.ecahack.busstop.activities;

import android.annotation.TargetApi;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.os.HandlerThread;
import android.os.Looper;
import android.util.Log;

import com.actionbarsherlock.app.SherlockFragmentActivity;

public abstract class BaseBusStopActivity extends SherlockFragmentActivity {

	private static final String TAG = "BaseBusStopActivity";

	private LocationManager mLocManager;
	private Location mCurrentLocation;
	private LocationListener mLocListener = new LocationListenerImpl();
	private LocationUpdateSettings mDefaultSettings;
	private boolean mGpsEnabled;
	private boolean mNetworkEnabled;
	private HandlerThread mLocationListenerThread;
	private int mFixCount;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		mLocManager = (LocationManager) getSystemService(LOCATION_SERVICE);
		mGpsEnabled = isProviderEnabled(LocationManager.GPS_PROVIDER);
		mNetworkEnabled = isProviderEnabled(LocationManager.NETWORK_PROVIDER);
	}

	private LocationUpdateSettings getDefaultSettings() {
		if (mDefaultSettings == null) {
			Log.v(TAG, "Requested default LocationUpdateSettings");
			mDefaultSettings = new LocationUpdateSettings();
			mDefaultSettings.minDist = 0;
			mDefaultSettings.minTime = 0;
			mDefaultSettings.provider = LocationUpdateSettings.PROVIDER_GPS_AND_NETWORK;
			mDefaultSettings.accuracySignificanceDelta = 50; // meters
			mDefaultSettings.timeSignificanceDelta = 3 * 60 * 1000; // ms i.e. two minutes
			mDefaultSettings.locationWaitTimeout = 14 * 1000; // 
		}
		return mDefaultSettings;
	}

	public LocationManager getLocationManager() {
		if (mLocManager == null) {
			throw new IllegalStateException(getClass() + ".onCreate has not been called yet");
		}
		return mLocManager;
	}

	/** Gets current best location, this will block for 8 seconds (Default) **/
	public Location getCurrentLocation() {
		startLocationUpdating(getDefaultSettings());
		waitForGoodLocation();
		stopLocationUpdating();
		return mCurrentLocation;
	}

	private void waitForGoodLocation() {
		if (!mGpsEnabled && !mNetworkEnabled) {
			Log.w(TAG, "Neither GPS nor NETWORK provider is enabled");
			return;
		}
		int timeSpent = 0;
		long timeout = getDefaultSettings().locationWaitTimeout;
		Log.v(TAG, "Waiting for location");
		while (mCurrentLocation == null && timeSpent <= timeout) {
			try {
				Thread.sleep(100);
				timeSpent += 100;
			} catch (InterruptedException e) {
				
			}
		}
		if (mCurrentLocation == null) {
			Log.w(TAG, "Unable to get current location");
		} else {
			Log.i(TAG, "Got location after " + timeSpent + "ms since request");
		}
	}
	
	public boolean isGpsEnabled() {
		return mGpsEnabled;
	}

	public boolean isNetworkEnabled() {
		return mNetworkEnabled;
	}

	private void startLocationUpdating(LocationUpdateSettings settings) {
		mCurrentLocation = null;
		mFixCount = 0;
		mLocationListenerThread = new HandlerThread("location-listener@" + System.currentTimeMillis());
		mLocationListenerThread.start();
		if (settings.provider.equals(LocationUpdateSettings.PROVIDER_GPS_AND_NETWORK)) {
			startNetworkUpdating(settings.minTime, settings.minDist);
			startGpsUpdating(settings.minTime, settings.minDist);
		} else if (settings.provider.equals(LocationManager.GPS_PROVIDER)) {
			startGpsUpdating(settings.minTime, settings.minDist);
		} else if (settings.provider.equals(LocationManager.NETWORK_PROVIDER)) {
			startNetworkUpdating(settings.minTime, settings.minDist);
		} else {
			throw new RuntimeException("Unknown provider");
		}
	}
	
	private void stopLocationUpdating() {
		mLocManager.removeUpdates(mLocListener);
		if (mLocationListenerThread !=null) {
			mLocationListenerThread.getLooper().quit();
			mLocationListenerThread = null;
		}
		Log.i(TAG, "Stopped location updates, received " + mFixCount + " fix count");
	}

	private void startNetworkUpdating(long minTime, float minDist) {
		if (!mNetworkEnabled) {
			Log.v(TAG, "Network provider is disabled");
			return;
		}
		Looper looper = mLocationListenerThread.getLooper();
		String network = LocationManager.NETWORK_PROVIDER;
		mLocManager.requestLocationUpdates(network, minTime, minDist, mLocListener, looper);
		Log.i(TAG, "Started location updates with NETWORK provider");
	}

	private void startGpsUpdating(long minTime, float minDist) {
		if (!mGpsEnabled) {
			Log.v(TAG, "Gps provider is disabled");
			return;
		}
		Looper looper = mLocationListenerThread.getLooper();
		String gps = LocationManager.GPS_PROVIDER;
		mLocManager.requestLocationUpdates(gps, minTime, minDist, mLocListener, looper);
		Log.i(TAG, "Started location updates with GPS provider");
	}

	private synchronized void updateCurrentLocationIfNeeded(Location location) {
		if (isBetterThanCurrentLocation(location)) {
			mCurrentLocation = location;
		}
		mFixCount++;
	}

	private boolean isProviderEnabled(String provider) {
		return mLocManager.isProviderEnabled(provider);
	}
	
	private boolean isBetterThanCurrentLocation(Location location) {
		if (mCurrentLocation == null) {
			Log.v(TAG, "First time set current location");
			return true;
		}
		LocationUpdateSettings settings = getDefaultSettings();
		long newTime = getSupportTime(location);
		long currentTime = getSupportTime(location);
		boolean isSignificantlyOlder = newTime - currentTime < -settings.timeSignificanceDelta;
		if (isSignificantlyOlder) {
			Log.v(TAG, "New location fix is significantly older than current, rejecting...");
			return false;
		}
		boolean isSignificantlyNewer = newTime - currentTime > settings.timeSignificanceDelta;
		if (isSignificantlyNewer) {
			Log.v(TAG, "New location fix is significantly newer than current, accepting...");
			return true;
		}
		float newAccuracy = location.getAccuracy();
		float currentAccuracy = mCurrentLocation.getAccuracy();
		boolean isSignificantlyInaccurate = newAccuracy - currentAccuracy > settings.accuracySignificanceDelta;
		if (isSignificantlyInaccurate) {
			Log.v(TAG, "New location fix is significantly inaccurate, rejecting...");
			return false;
		}
		Log.v(TAG, "New location fix is fairly accurate, accepting");
		return true;
	}

	@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
	private long getSupportTime(Location loc) {
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
			return loc.getElapsedRealtimeNanos();
		} else {
			return loc.getTime();
		}
	}

	static class LocationUpdateSettings {
		static final String PROVIDER_GPS_AND_NETWORK = "gps_network";
		long minTime;
		float minDist;
		String provider;
		long timeSignificanceDelta;
		float accuracySignificanceDelta;
		long locationWaitTimeout; // ms
	}

	class LocationListenerImpl implements LocationListener {

		@Override
		public void onLocationChanged(Location location) {
			updateCurrentLocationIfNeeded(location);
		}

		@Override
		public void onProviderDisabled(String provider) {
			Log.v(TAG, "Provider: " + provider + " is disabled");
		}

		@Override
		public void onProviderEnabled(String provider) {
			Log.v(TAG, "Provider: " + provider + " is enabled");
		}

		@Override
		public void onStatusChanged(String provider, int status, Bundle extras) {

		}

	}

}
